S03-08 JS-高级-事件循环、错误处理、Storage、正则、手写
[TOC]
事件循环@
概念
进程和线程
线程和进程是操作系统中的两个概念:
进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式;
线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中;
通俗解释:听起来很抽象,这里还是给出我的解释:
进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程);
线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程;
所以我们也可以说进程是线程的容器;
举例解释:再用一个形象的例子解释:
操作系统类似于一个大工厂;
工厂中里有很多车间,这个车间就是进程;
每个车间可能有一个以上的工人在工厂,这个工人就是线程;
图解:
操作系统的工作方式:
操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?
这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
当我们进程中的线程获取到时间片时,就可以快速执行我们编写的代码;
对于用户来说是感受不到这种快速的切换的;
你可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程:
浏览器和JavaScript
JavaScript是单线程:
我们经常会说JavaScript是单线程(可以开启workers) 的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node。
浏览器是一个进程吗,它里面只有一个线程吗?
目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出;
每个进程中又有很多的线程,其中包括执行JavaScript代码的线程;
JS代码是在一个单独的线程中执行的:
这就意味着JavaScript的代码,在同一个时刻只能做一件事;
如果这件事是非常耗时的,就意味着当前的线程就会被阻塞;
所以真正耗时的操作,实际上并不是由JavaScript线程在执行的:
浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作;
比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可;
阻塞IO和非阻塞IO
如果我们希望在程序中对一个文件进行操作,那么我们就需要打开这个文件:通过文件描述符。
- 我们思考:JavaScript 可以直接对一个文件进行操作吗?
- 看起来是可以的,但是事实上我们任何程序中的文件操作都是需要进行系统调用(操作系统封装了文件系统);
- 事实上对文件的操作,是一个操作系统的 IO 操作(输入、输出);
操作系统为我们提供了阻塞式调用和非阻塞式调用:
- 阻塞式调用: 调用结果返回之前,当前线程处于阻塞态(阻塞态 CPU 是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行。
- 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。
所以我们开发中的很多耗时操作,都可以基于这样的 非阻塞式调用
:
- 比如网络请求本身使用了 Socket 通信,而 Socket 本身提供了 select 模型,可以进行
非阻塞方式的工作
; - 比如文件读写的 IO 操作,我们可以使用操作系统提供的
基于事件的回调机制
;
但是非阻塞 IO 也会存在一定的问题:我们并没有获取到需要读取(我们以读取为例)的结果
- 那么就意味着为了可以知道是否读取到了完整的数据,我们需要频繁的去确定读取到的数据是否是完整的;
- 这个过程我们称之为轮训操作;
那么这个轮训的工作由谁来完成呢?
- 如果我们的主线程频繁的去进行轮训的工作,那么必然会大大降低性能;
- 并且开发中我们可能不只是一个文件的读写,可能是多个文件;
- 而且可能是多个功能:网络的 IO、数据库的 IO、子进程调用;
libuv 提供了一个线程池(Thread Pool):
- 线程池会负责所有相关的操作,并且会通过轮训等方式等待结果;
- 当获取到结果时,就可以将对应的回调放到事件循环(某一个事件队列)中;
- 事件循环就可以负责接管后续的回调工作,告知 JavaScript 应用程序执行对应的回调函数;
Event loop in node.js
阻塞和非阻塞,同步和异步有什么区别?
阻塞和非阻塞是对于被调用者来说的;
- 在我们这里就是系统调用,操作系统为我们提供了阻塞调用和非阻塞调用;
同步和异步是对于调用者来说的;
- 在我们这里就是自己的程序;
- 如果我们在发起调用之后,不会进行其他任何的操作,只是等待结果,这个过程就称之为同步调用;
- 如果我们再发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行,这个过程就是异步调用;
宏任务
宏任务(Macro Task): 是 JavaScript 事件循环中的一种异步任务类型,用于处理需要稍后执行的代码块。它的核心特点是:在事件循环的下一轮中执行,且优先级低于微任务。
本质:代表一个独立的、完整的代码执行单元。
触发时机:在事件循环的每一轮(Tick)中,执行完当前所有微任务后,从宏任务队列中取出一个任务执行。
设计目的:处理非紧急任务(如延迟操作、I/O 回调、用户交互事件),避免阻塞主线程。
常见来源:
- JS 主代码块:初始的
<script>
标签代码(本质上是第一个宏任务) - 定时器:
setTimeout
、setInterval
- I/O 操作:文件读写、网络请求(如
fetch
的回调) - DOM 事件:
click
、scroll
、resize
等事件回调 - UI 渲染:浏览器自动触发的渲染流程(如重绘、布局)
- requestAnimationFrame:动画回调(部分浏览器将其归类为宏任务)
微任务
微任务(Micro Task):是 JavaScript 事件循环中优先级最高的异步任务类型,用于处理需要立即执行的高优先级操作。它的核心特点是:在当前宏任务执行完毕后、下一个宏任务开始前,一次性清空所有微任务。
本质:代表一个需要尽快执行的轻量级任务。
触发时机:在每次宏任务执行结束后,立即清空微任务队列(包括嵌套生成的微任务)。
设计目的:处理需要即时响应的操作(如数据更新后的回调),确保在渲染前完成关键任务。
常见来源:
- Promise 回调:
Promise.then()
、Promise.catch()
、Promise.finally()
- queueMicrotask:显式添加微任务:
queueMicrotask(() => { ... })
- MutationObserver:监听 DOM 变化的回调(如元素属性、子节点变动)
- Node.js 环境特有:
process.nextTick()
(优先级甚至高于普通微任务)
关键特性:
高优先级:微任务队列的优先级高于宏任务队列,必须彻底清空后才会处理下一个宏任务。
完全清空:即使微任务中生成新的微任务(如嵌套
Promise.then
),也会持续执行,直到队列为空。渲染前执行:微任务在页面渲染前执行,适合处理需要即时生效的操作(如更新 DOM 后立即读取布局属性)。
示例:
console.log("1. 主线程开始");
// 宏任务
setTimeout(() => console.log("5. 宏任务"));
// 微任务
Promise.resolve().then(() => {
console.log("3. 微任务");
// 嵌套微任务
Promise.resolve().then(() => console.log("4. 嵌套微任务"));
});
console.log("2. 主线程结束");
面试题:Promise 面试题
面试题:Promise async await 面试题
浏览器的事件循环
浏览器的事件循环(Event Loop):是 JavaScript 在单线程环境下实现异步编程的核心机制。它通过协调 调用栈、任务队列 和 渲染管道,确保代码执行不阻塞主线程,同时高效处理用户交互、网络请求和页面渲染。
它是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现。
核心组成:
- 调用栈(Call Stack)
- 按顺序执行同步代码(后进先出,LIFO)。
- 当函数被调用时推入栈顶,执行完毕后弹出。
- 若栈被长时间占用(如死循环),页面会卡死(阻塞)。
- 任务队列(Task Queues)
- 宏任务队列(Macro Task Queue):存放
setTimeout
、setInterval
、I/O
、事件回调等任务。 - 微任务队列(Micro Task Queue):存放
Promise.then
、MutationObserver
、queueMicrotask
等高优先级任务。 - 其他队列:如
requestAnimationFrame
回调队列(与渲染相关)。
- 宏任务队列(Macro Task Queue):存放
- 渲染管道(Rendering Pipeline)
- 浏览器在合适的时机执行样式计算、布局(Layout)、绘制(Paint)等操作,更新页面显示。
事件循环的作用:
- 单线程的挑战:JavaScript 只有一个主线程,所有代码依次执行,若遇到耗时操作(如网络请求),页面会卡死。
- 解决方案:事件循环将异步任务交给浏览器其他线程处理,任务完成后将回调放入队列,主线程空闲时按规则执行队列中的任务。
工作流程:
事件循环的每一次迭代称为一个 “Tick”,其执行顺序如下:
执行一个宏任务
- 从宏任务队列中取出最旧的任务(如初始的
script
代码块)。 - 执行同步代码,遇到异步任务时:
- 宏任务(如
setTimeout
)的回调放入宏任务队列。 - 微任务(如
Promise.then
)的回调放入微任务队列。
- 宏任务(如
- 从宏任务队列中取出最旧的任务(如初始的
清空微任务队列
- 当前宏任务执行完毕后,立即依次执行微任务队列中的所有任务,直到队列为空。
- 注意:如果在处理微任务时又产生了新的微任务,会继续执行,直到彻底清空。
渲染页面(如果需要)
- 浏览器根据刷新率(通常 60Hz,约 16.6ms/帧)决定是否渲染。
- 执行与渲染相关的操作:
requestAnimationFrame
回调(在渲染前执行动画逻辑)。- 浏览器进行 样式计算 → 布局(Layout)→ 绘制(Paint)。
- 若时间充裕,可能执行
requestIdleCallback
(空闲时处理低优先级任务)。
取下一个宏任务
- 重复上述流程,形成循环。
关键特性:
微任务优先级高于宏任务
- 每个宏任务执行后,必须清空所有微任务才会处理下一个宏任务。
requestAnimationFrame
的定位- 其回调在渲染前执行,适合处理与动画相关的逻辑,不属于宏任务或微任务。
避免阻塞主线程
- 长时间运行的同步代码(如大数据循环)会阻塞事件循环,导致页面无响应。
- 优化方案:将任务拆分为多个小任务,通过
setTimeout
或queueMicrotask
分批执行。
宏任务的最小延迟
setTimeout(fn, 0)
的实际延迟至少为 4ms(浏览器规范限制)。
示例:
console.log("1. 主线程开始");
// 宏任务
setTimeout(() => console.log("4. 宏任务(setTimeout)"), 0);
// 微任务
Promise.resolve().then(() => console.log("3. 微任务(Promise)"));
console.log("2. 主线程结束");
Node的事件循环
Node的事件循环(Event Loop):是其非阻塞 I/O 和异步操作的核心机制,基于 libuv 库 实现。与浏览器的事件循环不同,Node 的事件循环采用 分阶段处理模型,将不同类型的任务分配到特定阶段执行。
libuv:是一个多平台的专注于异步IO的库,最初是为Node开发的,现在也被使用到Luvit、Julia、pyuv等其他地方。
图解:
- libuv 中主要维护了一个EventLoop和worker threads(线程池);
- EventLoop 负责调用系统的一些其他操作:文件的IO、Network、child-processes等
宏任务的六个阶段:
Node事件循环会将宏任务按顺序执行以下阶段,每个阶段处理特定类型的任务:
- Timers:执行
setTimeout()
和setInterval()
的回调。 - Pending I/O:处理上一轮循环中延迟的 I/O 回调(如系统错误回调ECONNREFUSED)。
- Idle/Prepare:Node.js 内部使用的阶段(开发者一般无需关注)。
- Poll:检索新的 I/O 事件,执行 I/O 回调(如文件读取、网络请求),其他的宏任务基本都在此阶段执行。
- Check:执行
setImmediate()
的回调。 - Close Callbacks:执行关闭事件的回调(如
socket.on('close', ...)
)。
执行流程:
┌───────────────────────┐
│ Timers │ ← 执行到期的定时器回调(setTimeout/setInterval)
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ Pending I/O Callbacks │ ← 执行系统操作(如TCP错误)的回调
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ Idle/Prepare │ ← Node.js 内部使用
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ Poll │ ← 等待新I/O事件,执行I/O回调
│ │ 如果队列为空:
│ │ - 如有setImmediate,进入Check阶段
│ │ - 否则等待新事件(阻塞)
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ Check │ ← 执行setImmediate回调
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ Close Callbacks │ ← 执行关闭事件的回调(如socket.close)
└───────────────────────┘
微任务的执行时机:
Node 的微任务分为两种,执行优先级高于宏任务:
- process.nextTick队列:在每个阶段结束后立即执行,优先级最高。
- 其他队列:如 Promise回调、queueMicrotask,它们会在
process.nextTick
队列清空后执行。
示例:
setTimeout(() => console.log('3. Timeout'), 0);
setImmediate(() => console.log('4. Immediate'));
Promise.resolve().then(() => console.log('2. Promise'));
process.nextTick(() => console.log('1. NextTick'));
// 注意:如果在I/O周期内初始化,可能先执行Immediate,再执行Timeout
面试题:
面试题一:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'))
process.nextTick(() => console.log('nextTick1'))
async1()
process.nextTick(() => console.log('nextTick2'))
new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
执行结果如下:
script start
async1 start
async2
promise1
promise2
script end
nextTick
async1 end
promise3
setTimeout0
setImmediate
setTimeout2
面试题二:
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
执行结果:
情况一:
setTimeout
setImmediate
情况二:
setImmediate
setTimeout
为什么会出现不同的情况呢?
- 在 Node 源码的 deps/uv/src/timer.c 中 141 行,有一个
uv__next_timeout
的函数; - 这个函数决定了,poll 阶段要不要阻塞在这里;
- 阻塞在这里的目的是当有异步 IO 被处理时,尽可能快的让代码被执行;
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
// 计算距离当前时间节点最小的计时器
heap_node = heap_min(timer_heap(loop));
// 如果为空, 那么返回-1,表示为阻塞状态
if (heap_node == NULL)
return -1; /* block indefinitely */
// 如果计时器的时间小于当前loop的开始时间, 那么返回0
// 继续执行后续阶段, 并且开启下一次tick
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout <= loop->time)
return 0;
// 如果不大于loop的开始时间, 那么会返回时间差
diff = handle->timeout - loop->time;
if (diff > INT_MAX)
diff = INT_MAX;
return (int) diff;
}
和上面有什么关系呢?
情况一:如果事件循环开启的时间(ms)是小于
setTimeout
函数的执行时间的;- 也就意味着先开启了 event-loop,但是这个时候执行到 timer 阶段,并没有定时器的回调被放到入 timer queue 中;
- 所以没有被执行,后续开启定时器和检测到有 setImmediate 时,就会跳过 poll 阶段,向后继续执行;
- 这个时候是先检测
setImmediate
,第二次的 tick 中执行了 timer 中的setTimeout
;
情况二:如果事件循环开启的时间(ms)是大于
setTimeout
函数的执行时间的;- 这就意味着在第一次 tick 中,已经准备好了 timer queue;
- 所以会直接按照顺序执行即可;
throw、try catch
错误处理方案
开发中我们会封装一些工具函数,封装之后给别人使用:
在其他人使用的过程中,可能会传递一些参数;
对于函数来说,需要对这些参数进行验证,否则可能得到的是我们不想要的结果;
很多时候我们可能验证到不是希望得到的参数时,就会直接return:
但是return存在很大的弊端:调用者不知道是因为函数内部没有正常执行,还是执行结果就是一个undefined;
事实上,正确的做法应该是如果没有通过某些验证,那么应该让外界知道函数内部报错了;
如何可以让一个函数告知外界自己内部出现了错误呢?
- 通过throw关键字,抛出一个异常;
throw语句:
throw语句用于抛出一个用户自定义的异常;
当遇到throw语句时,当前的函数执行会被停止(throw后面的语句不会执行);
如果我们执行代码,就会报错,拿到错误信息的时候我们可以及时的去修正代码。
throw关键字
throw表达式就是在throw后面可以跟上一个表达式来表示具体的异常信息:
throw关键字可以跟上哪些类型呢?
基本数据类型:比如number、string、Boolean
对象类型:对象类型可以包含更多的信息
但是每次写这么长的对象又有点麻烦,所以我们可以创建一个类:
Error类型
事实上,JavaScript已经给我们提供了一个Error类,我们可以直接创建这个类的对象:
Error包含三个属性:
messsage:创建Error对象时传入的message;
name:Error的名称,通常和类的名称一致;
stack:整个Error的错误信息,包括函数的调用栈,当我们直接打印Error对象时,打印的就是stack;
Error有一些自己的子类:**
RangeError:下标值越界时使用的错误类型;
SyntaxError:解析语法错误时使用的错误类型;
TypeError:出现类型错误时,使用的错误类型;
异常的处理
我们会发现在之前的代码中,一个函数抛出了异常,调用它的时候程序会被强制终止:
这是因为如果我们在调用一个函数时,这个函数抛出了异常,但是我们并没有对这个异常进行处理,那么这个异常会继续传递到上一个函数调用中;
而如果到了最顶层(全局)的代码中依然没有对这个异常的处理代码,这个时候就会报错并且终止程序的运行;
我们先来看一下这段代码的异常传递过程:
foo函数在被执行时会抛出异常,也就是我们的bar函数会拿到这个异常;
但是bar函数并没有对这个异常进行处理,那么这个异常就会被继续传递到调用bar函数的函数,也就是test函数;
但是test函数依然没有处理,就会继续传递到我们的全局代码逻辑中;
依然没有被处理,这个时候程序会终止执行,后续代码都不会再执行了;
异常的捕获
但是很多情况下当出现异常时,我们并不希望程序直接退出,而是希望可以正确的处理异常:
- 这个时候我们就可以使用try catch
在ES10(ES2019)中,catch后面绑定的error可以省略。
当然,如果有一些必须要执行的代码,我们可以使用finally来执行:
- finally表示最终一定会被执行的代码结构;
注意:如果try和finally中都有返回值,那么会使用finally当中的返回值;
Storage
正则表达式
防抖、节流
简介
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。
但是很多前端开发者面对这两个功能,有点摸不着头脑:
某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);
某些开发者可以区分,但是不知道如何应用;
某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;
接下来我们会一起来学习防抖和节流函数:
我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;
并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;
防抖函数
防抖函数(debounce)
我们用一副图来理解一下它的过程:
当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
当事件密集触发时,函数的触发会被频繁的推迟;
只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
应用场景:
防抖的应用场景很多:
搜索联想:
oninput
,输入框中频繁的输入内容,搜索或者提交信息;频繁点击事件:
onclick
,频繁的点击按钮,触发某个事件;浏览器滚动事件:
onscroll
,监听浏览器滚动事件,完成某些特定操作;浏览器缩放事件:
onresize
,用户缩放浏览器的resize事件;
示例: 搜索联想
我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容:
比如想要搜索一个MacBook:
当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
当继续输入ma时,再次发送网络请求;
那么macbook一共需要发送7次网络请求;
这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;
但是我们需要这么多次的网络请求吗?
不需要,正确的做法应该是在合适的情况下再发送网络请求;
比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;
比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;
也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;
这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;
节流函数
节流函数(throttle)
我们用一副图来理解一下节流的过程
当事件触发时,会执行这个事件的响应函数;
如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;
应用场景:
页面滚动事件:监听页面的滚动事件;
鼠标移动事件;
频繁点击事件:用户频繁点击按钮操作;
游戏某些设计:游戏中的一些设计,如发射子弹;
很多人都玩过类似于飞机大战的游戏
在飞机大战的游戏中,我们按下空格会发射一个子弹:
很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;
比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;
但是事件是触发了10次的,响应的函数只触发了一次;
生活中的例子
生活中防抖的例子:
比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);
生活中节流的例子:
比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;
如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;
案例准备
我们通过一个搜索框来延迟防抖函数的实现过程:
- 监听input的输入,通过打印模拟网络请求
测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:
underscore
Underscore库的介绍
事实上我们可以通过一些第三方库来实现防抖操作:
lodash
underscore
这里使用underscore
我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
但是目前我看到underscore还在维护,lodash已经很久没有更新了;
Underscore的官网: https://underscorejs.org/
安装:
Underscore的安装有很多种方式:
下载Underscore,本地引入;
通过CDN直接引入;
通过包管理工具(npm)管理安装;
这里我们直接通过CDN:
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
Underscore实现防抖和节流
手写题
手写-防抖函数
我们按照如下思路来实现:
- 防抖基本功能实现:可以实现防抖效果
- 优化一:优化参数和this指向
- 优化二:优化取消操作(增加取消功能)
- 优化三:优化立即执行效果(第一次立即执行)
- 优化四:优化返回值
1、基本实现
2、优化:参数和this绑定
this指向
参数
3、优化:取消功能
4、优化:第一次立即执行
immediate
:控制否时启用立即执行功能isInvoke
:控制函数是否已经立即执行一次了
5、优化:返回值
手写-节流函数
我们按照如下思路来实现:
- 节流函数的基本实现:可以实现节流效果
- 优化一:绑定this和参数
- 优化二:控制立即执行,节流最后一次也可以执行
- 优化三:优化添加取消功能
- 优化四:优化返回值问题
1、基本实现
2、优化:绑定this和参数
3、优化:控制立即执行
4、优化:控制执行最后一次
思路一: 给每次点击时添加一个定时器,延迟时间设为waitTime,当再次点击时取消上次的定时器,重新添加一个。
思路二: 在每个执行fn函数的节点,添加一个延迟时间为waitTime的定时器,当用户在fn函数执行节点的时间上也点击了一次就取消该定时器(使用中)
4、优化:取消功能
5、优化:返回值
手写-深拷贝函数
前面我们已经学习了对象相互赋值的一些关系,分别包括:
引用赋值:指向同一个对象,相互之间会影响;
对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
对象的深拷贝:两个对象不再有任何关系,不会相互影响;
深拷贝实现方式:
- JSON.parse
- 第三方库:underscore、lodash
- 自己实现
前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse
这种深拷贝的方式其实对于函数、Symbol等是无法处理的;
并且如果存在对象的循环引用,也会报错的;
const obj = JSON.parse(JSON.stringify(info))
自定义深拷贝函数:
1.自定义深拷贝的基本功能;
2.对Symbol的key进行处理;
3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;
4.对循环引用的处理;
工具函数:判断对象
1、基本实现
2、优化:区分数组和对象
3、优化:其他类型-处理set
4、优化:其他类型-处理map
5、优化:其他类型-处理function
function: 不需要深拷贝
6、优化:其他类型-处理Symbol为值
7、优化:其他类型-处理Symbol为key
8、优化:处理循环引用
方案一:将每次新创建的对象保存到Map中,每次遍历前判断之前是否已经保存过了该对象
问题:需要在deeCopy外部定义一个map,并且每次拷贝完成后map依然会形成对对象的强引用,没有销毁
方案二(推荐):使用WeakMap替代Map;将map放入参数中并设置一个默认值new WeakMap()
手写-事件总线
自定义事件总线属于一种观察者模式,其中包括三个角色:
发布者(Publisher):发出事件(Event);
订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;
当然我们可以选择一些第三方库:
Vue2默认是带有事件总线的功能;
Vue3中推荐一些第三方库,比如mitt;
当然我们也可以实现自己的事件总线:
事件的监听方法on;
事件的发射方法emit;
事件的取消监听off;
1、基本实现
2、优化:绑定参数
3、优化:移除监听